Add SDK MCP OAuth host token handlers#1669
Conversation
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 2.1M
| export interface McpAuthToken { | ||
| /** Access token acquired by the SDK host. */ | ||
| accessToken: string; | ||
| /** OAuth token type. Defaults to Bearer when omitted. */ | ||
| tokenType?: string; | ||
| /** Refresh token supplied by the host, if available. */ | ||
| refreshToken?: string; | ||
| /** Token lifetime in seconds, if known. */ | ||
| expiresIn?: number; | ||
| } |
There was a problem hiding this comment.
what about an idToken? idTokens are useful as they contain claims that are good to display like usernames/email/etc.
There was a problem hiding this comment.
So unless I'm mistaken, the runtime wouldn't have any actual use for it; all it needs as a response from the host is the access token to be forwarded back to the MCP server for access... In fact, I'm about to remove the refreshToken from here, since as discussed in yesterday's meeting, refresh would be the host's responsibility, and so the runtime has no use for that.
But let me know if I have the wrong mental model here! Plus we can always add it later.
59694fd to
6d3d655
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 1.8M
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 3.1M
a4cfe07 to
e3aae80
Compare
This comment has been minimized.
This comment has been minimized.
530ce4b to
a16180e
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 2.7M
1ef3726 to
87f1785
Compare
This comment has been minimized.
This comment has been minimized.
51c9235 to
d5c709b
Compare
d5c709b to
70ec1fd
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 2.3M
70ec1fd to
672611c
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
This PR synchronizes all language SDKs in this mono-repo (Node/TS, Python, Go, .NET, Java, Rust) with the runtime’s MCP OAuth lifecycle contract by adding a host-delegated MCP OAuth callback API, wiring it through session event handling, and expanding cross-language E2E coverage using a shared OAuth-protected MCP test fixture.
Changes:
- Add MCP OAuth host callback APIs/types (request + token/cancel result) and wire them to
mcp.oauth_requiredsession events, responding viasession.mcp.oauth.handlePendingRequest. - Register
mcp.oauth_requiredevent interest only when an MCP auth handler/callback is configured (create + resume; cloud create registers after server-assigned session id). - Add a minimal OAuth-protected MCP HTTP test server plus new/expanded E2E + unit tests across languages to validate initial auth, refresh/replacement, upscope, reauth, and cancellation.
Show a summary per file
| File | Description |
|---|---|
| test/harness/test-mcp-oauth-server.mjs | New minimal OAuth-protected MCP server fixture for cross-SDK E2E coverage. |
| rust/tests/session_test.rs | Adds Rust unit tests for optional MCP OAuth metadata and interest registration behavior. |
| rust/tests/e2e/support.rs | Enables MCP Apps flags in Rust E2E environment. |
| rust/tests/e2e/mcp_oauth.rs | New Rust E2E test validating MCP OAuth lifecycle with host-provided tokens. |
| rust/tests/e2e.rs | Registers the new Rust MCP OAuth E2E module. |
| rust/src/types.rs | Adds mcp_auth_handler to Rust session configs (create + resume). |
| rust/src/session.rs | Registers MCP OAuth event interest and dispatches MCP OAuth required events to the handler. |
| rust/src/handler.rs | Introduces Rust MCP auth request/result types + wire conversion. |
| python/test_client.py | Adds Python unit tests for MCP auth interest registration and handler dispatch. |
| python/e2e/testharness/context.py | Enables MCP Apps flags in Python E2E harness environment. |
| python/e2e/test_mcp_oauth_e2e.py | New Python E2E test validating MCP OAuth lifecycle with host-provided tokens. |
| python/copilot/session.py | Adds MCP auth typed callback surface + event dispatch/response plumbing in sessions. |
| python/copilot/client.py | Wires MCP auth callback through create/resume and conditionally registers event interest. |
| python/copilot/init.py | Exposes new MCP auth types in the Python public package surface. |
| nodejs/test/e2e/provider_endpoint.e2e.test.ts | Updates harness usage to pass env via client options instead of mutating shared env. |
| nodejs/test/e2e/mcp_oauth.e2e.test.ts | New Node E2E test validating MCP OAuth lifecycle with host-provided tokens. |
| nodejs/test/e2e/harness/sdkTestContext.ts | Adds env override merging and marks test user as MCP-enabled; normalizes CLI path lookup. |
| nodejs/test/client.test.ts | Adds Node unit tests for MCP auth dispatch and conditional interest registration. |
| nodejs/src/types.ts | Adds Node public types for MCP auth request/result/callback and session config option. |
| nodejs/src/session.ts | Handles mcp.oauth_required events and responds via session.mcp.oauth.handlePendingRequest. |
| nodejs/src/client.ts | Wires MCP auth handler into sessions and conditionally registers MCP OAuth event interest. |
| java/src/test/java/com/github/copilot/McpOAuthE2ETest.java | New Java E2E test validating MCP OAuth lifecycle with host-provided tokens. |
| java/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java | New Java unit test verifying conditional interest registration + optional metadata exposure. |
| java/src/test/java/com/github/copilot/E2ETestContext.java | Enables MCP Apps flags in Java E2E environment. |
| java/src/main/java/com/github/copilot/SessionRequestBuilder.java | Registers the MCP auth handler on sessions for create + resume. |
| java/src/main/java/com/github/copilot/rpc/SessionConfig.java | Adds onMcpAuthRequest callback to Java session config and clones it. |
| java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java | Adds onMcpAuthRequest callback to Java resume config and clones it. |
| java/src/main/java/com/github/copilot/rpc/McpAuthToken.java | New Java public DTO for MCP OAuth token data. |
| java/src/main/java/com/github/copilot/rpc/McpAuthResult.java | New Java public DTO/factories for token vs cancellation. |
| java/src/main/java/com/github/copilot/rpc/McpAuthRequest.java | New Java public DTO for MCP OAuth request payload. |
| java/src/main/java/com/github/copilot/rpc/McpAuthInvocation.java | New Java invocation context object (session id). |
| java/src/main/java/com/github/copilot/rpc/McpAuthHandler.java | New Java functional interface for MCP OAuth callback. |
| java/src/main/java/com/github/copilot/CopilotSession.java | Dispatches MCP OAuth required events to handler and replies via pending-request RPC. |
| java/src/main/java/com/github/copilot/CopilotClient.java | Conditionally registers mcp.oauth_required interest for create + resume. |
| go/types.go | Adds Go public MCP auth types and callback signature; threads into session configs. |
| go/session.go | Dispatches MCP OAuth required events to a Go handler and responds via pending-request RPC. |
| go/session_test.go | Adds Go unit tests for MCP auth request propagation and response payloads. |
| go/internal/e2e/testharness/context.go | Enables MCP Apps flags in Go E2E harness environment. |
| go/internal/e2e/mcp_oauth_e2e_test.go | New Go E2E test validating MCP OAuth lifecycle with host-provided tokens. |
| go/client.go | Wires handler into sessions and conditionally registers MCP OAuth event interest. |
| go/client_test.go | Adds Go unit tests verifying conditional interest registration for create/resume/cloud. |
| dotnet/test/Unit/SessionEventSerializationTests.cs | Extends .NET event serialization tests for optional MCP OAuth metadata fields. |
| dotnet/test/Unit/PublicDtoTests.cs | Adds .NET DTO factory tests for MCP auth token vs cancellation results. |
| dotnet/test/Unit/ClientSessionLifetimeTests.cs | Adds .NET unit tests verifying conditional interest registration behavior. |
| dotnet/test/Harness/E2ETestContext.cs | Enables MCP Apps flags in .NET E2E environment. |
| dotnet/test/E2E/McpOAuthE2ETests.cs | New .NET E2E test validating MCP OAuth lifecycle with host-provided tokens. |
| dotnet/src/Types.cs | Adds .NET public MCP auth types and session config callback. |
| dotnet/src/Session.cs | Handles mcp.oauth_required events and responds via pending-request RPC. |
| dotnet/src/Client.cs | Registers MCP OAuth interest when a handler is configured (create + resume). |
Review details
- Files reviewed: 49/49 changed files
- Comments generated: 4
- Review effort level: Low
672611c to
1d608df
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
153a7d6 to
59b6d85
Compare
This comment has been minimized.
This comment has been minimized.
59b6d85 to
1d608df
Compare
| if (config.OnMcpAuthRequest is not null) | ||
| { | ||
| await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken); | ||
| } |
There was a problem hiding this comment.
Are these necessarily tied together?
I'm thinking back to permission requests and tool invocation requests. Initially the only way you could handle them was to provide a handler that the SDK would invoke, but that was prohibitive, especially with regards to needing to be able to suspend the runtime and later resume it (possibly on a different machine) and supply the result of the operation in order to keep going. We made it so that providing the callback was optional; if you don't supply it, you're on your own for handling the events and calling the runtime rpc method to supply the results.
Is there a correlary here?
There was a problem hiding this comment.
Yeah, at least at the moment, register interest in the callback is necessary since the runtime has two MCP OAuth flows - the new one being introduced here and the in-process one currently used by the CLI; I'm working on a PR to switch the CLI to use the new SDK flow - as part of the CLI-over-SDK effort - but for now the interest registration is required in order for the runtime to know which path to take.
Regardless, even after we remove the CLI flow, I'm not sure how the "you're on your own" approach would work here MCP OAuth... The LLM can request a tool call at an arbitrary time, which could trigger an "oauth_required" request, which must be satisfied by the host; if this doesn't happen, the tool call can't proceed, and the turn is stuck. So I think it means the host must be informed (and it must respond in a timely manner)... I think that this oauth flow must indeed be rewired upon resume (even on a different machine), but it doesn't change the fact that an OAuth request can happen at any point, and the host must be able to handle it...
In other words, I think that any SDK host that potentially needs to support OAuth-authorizing MCP servers must register this callback, and if such a request is emitted without that callback being registered, I'm not sure what choice we have besides a fail-fast error... But I might have an incorrect mental model here - let me know how you see this.
There was a problem hiding this comment.
To make sure I've understood correctly, all the SDK ends up doing is listening for a particular event, invoking the user's callback, and calling the exposed RPC method to supply the result, right? So SDK consumer could do the same thing without providing a callback: they could listen for the same event, do whatever they want in response (whatever they would have supplied in the callback), and then call that same RPC method, right?
The PR is using a callback being set as an indication that the client is handling oauth, but if the above is true, then those needn't be 1:1... supplying a callback could imply it, but not supplying a callback doesn't mean the client can't also handle it. My question is really: should there be a separate knob for opting-in to handling oauth beyond just supplying a callback.
It's fine if the answer is "not right now, we could always add one in the future if supplying a callback is problematic for some reason".
There was a problem hiding this comment.
EDIT: Maybe read my comment below, I think I understood better what you were asking
Here's how the flow works:
- The runtime makes an MCP call, and receives a 401/403 HTTP error, signifying that a (new) OAuth access token is required. Crucially, the runtime is the one seeing this - the hosting application (or client) has no idea it's happening.
- In the new host-delegating flow (when "interest is registered"), the runtime records the pending OAuth request and then invokes the callback in the hosting application, passing it various information from the 401 response (e.g. WWW-Authenticate header contents).
- The hosting application does whatever is needed in order to obtain the access token (user browser flow, or possibly just a refresh cycle).
- The hosting application then calls an API back into the SDK (handlePendingRequest)
- Back in the runtime, we correlate the request ID given to handlePendingRequest with the recorded OAuth request from step 2, and then redoes the MCP request with the new auth token received from the host.
IIUC you're asking whether it makes sense to have a host opt into host-delegated OAuth without registering a callback. In that case, I can't see what would happen when the runtime receives a 401/403 HTTP error which is the trigger for OAuth - how would the host application be made aware that OAuth needs to happen? Like are you thinking of some sort of polling mechanism where the host application would periodically check if there are any pending OAuth requests? I guess that's theoretically possible, but would at the very least mean lots of latency no?
Are you asking because reinstating host callbacks with the runtime after resume is problematic? I looked at this and it seems that we have several other callbacks which IIRC are should also be reinstated after resume - but I could be wrong.
Happy to do a quick call tomorrow to discuss all this!
There was a problem hiding this comment.
OK, I reread everything and I think I understand your concern... You're saying that in the SDK implementation here (not the runtime), we expose a single callback which must return the token; this would prevents e.g. an implementation that persists the various parameters to disk, and then some other processes loads those and calls handlePendingRequest with the token. In other words, this is less about separating the opt-in from the callback, it's more to have a more low-level split interaction where there's the callback invoked by the runtime doesn't have to return an access token "immediately", and the host application later invokes handlePendingRequest manually.
I can see that... And that's exactly how the low-level RPC API is actually structured (oauth_required is the low-level callback that returns nothing). If so, then I think all this should be achievable with the low-level APIs today:
await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required");
using var sub = session.On<McpOauthRequiredEvent>(evt =>
{
// Persist requestId + auth params.
// Do not return token; this is just an event callback.
});
// Later / elsewhere:
await session.Rpc.Mcp.Oauth.HandlePendingRequestAsync(requestId, tokenResult);There may be some issues around registering interest (and the callback) early enough during resume so as to avoid any race conditions... Also, specifically for the multiple process scenario (one process receives oauth_required, persists, another process actually completes with the access token), the runtime itself needs to remember/correlate the pending OAuth requests, and that's in-memory only; so a true resume will have lost that (assuming the runtime is in the same process as the host application).
tl;dr I think we have a nice high-level convenience callback for the common scenario (register a single async callback that returns the access token). In principle, users can still drop down to low-level RPC APIs to do the advanced (cross-process) thing, but it's likely we have some gaps around this - but they go beyond simple API shape here in the SDK, and we should probably think/design that separately (the main question is whether the high-level API as-is seems OK even if the advanced scenario isn't there yet).
Does this all make sense and correspond to what you're asking?
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 3M
1d608df to
7b32bb0
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 2.7M
Expose host-delegated MCP OAuth handling across SDK languages, sync generated RPC and event models to the lifecycle contract, and add cross-language E2E coverage for initial auth, refresh, upscope, reauth, and cancellation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7b32bb0 to
f78c387
Compare
Cross-SDK Consistency Review — MCP OAuth Host-Token HandlersReviewed all six SDK implementations (Node.js, Python, Go, .NET, Java, Rust) for the MCP OAuth host-token callback feature added in this PR. ✅ Overall: Highly ConsistentAll six SDKs implement the feature completely and consistently:
|
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1669 · sonnet46 3.7M
| #[derive(Debug, Clone)] | ||
| pub struct McpAuthRequest { | ||
| /// Identifier for the pending MCP OAuth request. | ||
| pub request_id: RequestId, |
There was a problem hiding this comment.
Cross-SDK consistency note: Unlike the other Rust handler data structs in this file — PermissionRequestData and ElicitationRequest neither of which include a request_id field — McpAuthRequest exposes pub request_id: RequestId here.
Because McpAuthHandler::handle already passes request_id as a dedicated top-level parameter (matching the PermissionHandler / ElicitationHandler pattern), this field is redundant: the same value is accessible via both request_id and request.request_id.
In every other SDK, requestId/request_id lives only inside the request object (it is never a separate parameter):
- Node.js/Python:
handler(request, { sessionId })—request.requestId - Go:
handler(request MCPAuthRequest, invocation MCPAuthInvocation)—request.RequestID - .NET:
handler(McpAuthContext)—context.RequestId(single unified object) - Java:
handle(McpAuthRequest request, McpAuthInvocation invocation)—request.requestId()
Consider removing request_id from McpAuthRequest to stay consistent with the Rust SDK's own PermissionRequestData/ElicitationRequest types and with the other language SDKs. If you'd like the struct to be self-contained (e.g. for storing the request after the handler returns), it's worth noting this departure in a doc comment.
Summary
mcp.oauth_requiredinterest when a host auth handler is configured, map runtime OAuth request events into idiomatic per-language handler contexts, and respond viasession.mcp.oauth.handlePendingRequestwith token or cancellation results.clientSecret, and OAuth completion events.Validation
Notes
idTokenreview thread is intentionally left unresolved for follow-up discussion.